# -*- coding: utf-8 -*-
#--------------------------------------------------------------------------
# Copyright (c) 2009 Barry Schwartz
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

#--------------------------------------------------------------------------

import traceback
cimport stdlib
from kt cimport libff

## |make_tag| is not needed at the moment.
#
#def make_tag(s):
#    if isinstance(s, str):
#        s = s.encode('iso-8859-1')
#    assert(len(s) == 4)
#    return s[3] + 256*(s[2] + 256*(s[1] + 256*s[0]))

def unmake_tag(t):
    s = bytearray(4)
    s[3] = t % 256
    t //= 256
    s[2] = t % 256
    t //= 256
    s[1] = t % 256
    t //= 256
    s[0] = t % 256
    return s.decode('iso-8859-1')

feature_RQD = ' RQD'

ocaml_lookup_types = {
    libff.gsub_single : 'SingleSubstitutionLookup',
    libff.gsub_multiple : 'MultipleSubstitutionLookup',
    libff.gsub_alternate : 'AlternateSubstitutionLookup',
    libff.gsub_ligature : 'LigatureSubstitutionLookup',
    libff.gsub_context : 'ContextSubstitutionLookup',
    libff.gsub_contextchain : 'ChainingContextSubstitutionLookup',
    libff.gsub_reversecchain : 'ReverseChainingContextSubstitutionLookup',
    libff.morx_indic : None,    # An Apple table; not implemented.
    libff.morx_context : None,  # An Apple table; not implemented.
    libff.morx_insert : None,   # An Apple table; not implemented.
    libff.gpos_single : 'SingleAdjustmentLookup',
    libff.gpos_pair : 'PairAdjustmentLookup',
    libff.gpos_cursive : 'CursiveAttachmentLookup',
    libff.gpos_mark2base : 'MarkToBaseAttachmentLookup',
    libff.gpos_mark2ligature : 'MarkToLigatureAttachmentLookup',
    libff.gpos_mark2mark : 'MarkToMarkAttachmentLookup',
    libff.gpos_context : 'ContextPositioningLookup',
    libff.gpos_contextchain : 'ChainedContextPositioningLookup',
    libff.kern_statemachine : None, # An Apple table; not implemented.
}

cdef inline get_value_record(libff.vr valrec):
    return (valrec.xoff, valrec.yoff, valrec.h_adv_off, valrec.v_adv_off)

subtab_type_string = {
    libff.pst_position     : 'SingleAdjustmentSubtable',
    libff.pst_pair         : 'PairAdjustmentSubtable',
    libff.pst_substitution : 'SingleSubstitutionSubtable',
    libff.pst_ligature     : 'LigatureSubstitutionSubtable',
}

cdef get_pair_positioning(gid_map,
                          libff.splinechar *sc,
                          libff.generic_pst *possub):
    # FIX (IMPORTANT!!!): Can fontforge handle absence of a value
    # record? Should all-zero second value records be treated as
    # absent?
    gid = sc.orig_pos
    if gid not in gid_map:
        gid_map[gid] = {}
    value_record1 = get_value_record(possub.u.pair.vr[0])
    value_record2 = get_value_record(possub.u.pair.vr[1])
    gid2 = libff.SFFindGID(sc.parent, -1, possub.u.pair.paired)
    gid_map[gid][gid2] = (value_record1, value_record2)

cdef get_alternate_substitution(gid_map,
                                libff.splinechar *sc,
                                libff.generic_pst *possub):
    s = possub.u.alt.components
    i = s.find(b' ')
    if 0 <= i:
        s = s[:i]           # Keep only the first (default) alternate.
    gid_map[sc.orig_pos] = libff.SFFindGID(sc.parent, -1, s)

cdef get_per_glyph_pst_data(libff.splinefont *sf, libff.lookup_subtable *subtab):

    cdef libff.splinechar *sc
    cdef libff.generic_pst *possub
    cdef int subtab_type
    cdef int i

    gid_map = {}
    subtab_type = libff.pst_null
    for i in range(sf.glyphcnt):
        sc = sf.glyphs[i]
        possub = sc.possub
        while possub is not NULL:
            if possub.subtable == subtab:
                if possub.pst_type == libff.pst_position:
                    gid_map[sc.orig_pos] = get_value_record(possub.u.pos)
                    subtab_type = libff.pst_position
                elif possub.pst_type == libff.pst_pair:
                    get_pair_positioning(gid_map, sc, possub)
                    subtab_type = libff.pst_pair
                elif possub.pst_type == libff.pst_substitution:
                    gid_map[sc.orig_pos] = libff.SFFindGID(sc.parent, -1, possub.u.subs.variant)
                    subtab_type = libff.pst_substitution
                elif possub.pst_type == libff.pst_alternate:
                    # Treat alternate substitution as single
                    # substitution by the first glyph listed (which is
                    # the default recommended by the OpenType spec).
                    get_alternate_substitution(gid_map, sc, possub)
                    subtab_type = libff.pst_substitution
            possub = possub.next
    return (subtab_type_string[subtab_type], gid_map) if len(gid_map) != 0 else None

# FIX: Add support for right-to-left and vertical kerning. (See fontforge/python.c)
cdef get_kerns(libff.splinefont *sf, libff.lookup_subtable *subtab):

    cdef libff.splinechar *sc
    cdef libff.kernpair *kp
    cdef int i

    gid_map = {}
    for i in range(sf.glyphcnt):
        sc = sf.glyphs[i]
        kp = sc.kerns
        while kp is not NULL:
            if kp.subtable is subtab:
                offset = kp.off
                gid1 = sc.orig_pos
                gid2 = kp.sc.orig_pos
                value_record = (0, 0, offset, 0)
                adjustment = (value_record, None)
                if gid1 not in gid_map:
                    gid_map[gid1] = {}
                gid_map[gid1][gid2] = adjustment
            kp = kp.next
    return (subtab_type_string[libff.pst_pair], gid_map) if len(gid_map) != 0 else None

cdef get_ligatures(libff.splinefont *sf, libff.lookup_subtable *subtab):
    cdef libff.splinechar *sc
    cdef libff.splinecharlist *p
    cdef libff.liglist *lig_list
    cdef int i

    libff.SFLigaturePrepare(sf)

    gid_map = {}
    for i in range(sf.glyphcnt):
        sc = sf.glyphs[i]
        ligatures = []
        lig_list = sc.ligofme
        while lig_list is not NULL:
            if lig_list.lig.subtable is subtab:
                components = []
                p = lig_list.components
                while p is not NULL:
                    components.append(p.sc.orig_pos)
                    p = p.next
                ligatures.append((components, lig_list.lig.u.lig.lig.orig_pos))
            lig_list = lig_list.next
        if len(ligatures) != 0:
            gid_map[sc.orig_pos] = ligatures

    libff.SFLigatureCleanup(sf)

    return (subtab_type_string[libff.pst_ligature], gid_map) if len(gid_map) != 0 else None

cdef get_simple_pair_positioning(libff.splinefont *sf, libff.lookup_subtable *subtab):
    result1 = get_per_glyph_pst_data(sf, subtab)
    result2 = get_kerns(sf, subtab)
    if result1 is None:
        result = result2
    elif result2 is None:
        result = result1
    else:
        (subtab_type, gid_map1) = result1
        (subtab_type, gid_map2) = result2
        gid_map1.update(gid_map2)
        result = (subtab_type, gid_map1)
    return result

cdef get_class_based_kerning(libff.splinefont *sf, libff.kernclass *kc):
    cdef int i

    firsts = {}
    for i in range(kc.first_cnt):
        # Skip an "everything else" row, because it is outside the
        # coverage set that is recognized by FontForge.
        if kc.firsts[i] is not NULL:
            for s in kc.firsts[i].split():
                firsts[libff.SFFindGID(sf, -1, s)] = i

    # I think the following assertion is the case in FontForge, but
    # let's put it in an assert statement, to help prove me wrong if I
    # am wrong.
    assert kc.seconds[0] is NULL

    seconds = {}
    all_seconds = set()
    for i in range(1, kc.second_cnt):
        for s in kc.seconds[i].split():
            seconds[libff.SFFindGID(sf, -1, s)] = i

    # Handle the "everything else" case for the second glyph.
    for i in range(sf.glyphcnt):
        if i not in seconds:
            seconds[i] = 0

    adjustments = [kc.offsets[i] for i in range(kc.first_cnt * kc.second_cnt)]

    return ('ClassBasedKerningSubtable',
            (kc.second_cnt, firsts, seconds, adjustments))

cdef int find_lookup_index(libff.otlookup *p, libff.otlookup *lookup):
    cdef int i = 0
    while lookup is not NULL and lookup is not p:
        i += 1
        p = p.next
    return i if lookup is not NULL else -1

cdef int find_lookup_index_in_font(libff.splinefont *sf, libff.otlookup *lookup):
    cdef int index
    # Looking through both lists probably is more than fast enough.
    index = find_lookup_index(sf.gsub_lookups, lookup)
    if index < 0:
        index = find_lookup_index(sf.gpos_lookups, lookup)
    return index

cdef get_chainsub_format3(libff.splinefont *sf, libff.generic_fpst *fpst):
    cdef int i, j, k
    cdef int lookup_index
    cdef libff.fpst_rule *rule
    cdef libff.seqlookup *seq
    cdef libff.fpv *coverage

    rule = &fpst.rules[0]

    coverage = &rule.u.coverage
    ncovers = [coverage.ncovers[k].split() for k in range(coverage.ncnt)]
    bcovers = [coverage.bcovers[k].split() for k in range(coverage.bcnt)]
    fcovers = [coverage.fcovers[k].split() for k in range(coverage.fcnt)]

    lookup_indexes = []
    for j in range(rule.lookup_cnt):
        seq = &rule.lookups[j]
        seq_index = seq.seq
        lookup_index = find_lookup_index_in_font(sf, seq.lookup)
        lookup_indexes.append((seq_index, lookup_index))

    record = {
        'backtrack_coverage' : [[libff.SFFindGID(sf, -1, s) for s in p] for p in bcovers],
        'input_coverage'     : [[libff.SFFindGID(sf, -1, s) for s in p] for p in ncovers],
        'lookahead_coverage' : [[libff.SFFindGID(sf, -1, s) for s in p] for p in fcovers],
        'lookup_indexes'     : lookup_indexes,
        }

    return ('ChainingContextSubstitution3Subtable', record)

cdef get_subtable(libff.splinefont *sf, libff.lookup_subtable *subtab):
    if subtab.per_glyph_pst_or_kern != 0:
        if (subtab.lookup.lookup_type == libff.gpos_single or
            subtab.lookup.lookup_type == libff.gsub_single or
            subtab.lookup.lookup_type == libff.gsub_alternate):
            result = get_per_glyph_pst_data(sf, subtab)
        elif subtab.lookup.lookup_type == libff.gpos_pair:
            result = get_simple_pair_positioning(sf, subtab)
        elif subtab.lookup.lookup_type == libff.gsub_ligature:
            result = get_ligatures(sf, subtab)
    elif subtab.kc is not NULL:
        result = get_class_based_kerning(sf, subtab.kc)
    elif subtab.fpst is not NULL:
        if subtab.fpst.fpst_type == libff.pst_chainsub:
            if subtab.fpst.format == libff.pst_coverage:
                result = get_chainsub_format3(sf, subtab.fpst)
    return result

cdef get_lookup_subtables(libff.splinefont *sf, libff.lookup_subtable *subtab):
    subtable_list = []
    while subtab is not NULL:
        subtab_record = get_subtable(sf, subtab)
        if subtab_record == None:
            name = subtab.subtable_name
            name = name.decode('UTF-8')
            print 'Subtable "' + name + '" cannot yet be handled' # We need something better than this!  ??????
        else:
            subtable_list.append(subtab_record)
        subtab = subtab.next
    return subtable_list

cdef get_lookups(libff.splinefont *sf, libff.otlookup *lookups):

    cdef libff.otlookup *lookup
    cdef libff.lookup_subtable *subtab

    lookup_list = []
    i = 0
    lookup = lookups

    while lookup is not NULL:
        subtable_list = get_lookup_subtables(sf, lookup.subtables)
        lookup_table = {
            'lookupType'          : ocaml_lookup_types[lookup.lookup_type],
            'rightToLeft'         : (lookup.lookup_flags & libff.pst_r2l) != 0,
            'ignoreBaseGlyphs'    : (lookup.lookup_flags & libff.pst_ignorebaseglyphs) != 0,
            'ignoreLigatures'     : (lookup.lookup_flags & libff.pst_ignoreligatures) != 0,
            'ignoreMarks'         : (lookup.lookup_flags & libff.pst_ignorecombiningmarks) != 0,
            'useMarkFilteringSet' : (lookup.lookup_flags & libff.pst_usemarkfilteringset) != 0,
            'markAttachmentType'  : (lookup.lookup_flags & libff.pst_markclass) >> 8,
            'subTables'           : subtable_list,
            }
        lookup_list.append(lookup_table)

        lookup = lookup.next
        i += 1
    return lookup_list

def script_lang_match(script, langsys, script_lang):
    i = 0
    while (i < len(script_lang) and
           (script != script_lang[i][0] or langsys not in script_lang[i][1])):
        i += 1
    return i < len(script_lang)

def get_features(f, lookup_names, script, langsys):
    features = {}
    i = 0
    for i in range(len(lookup_names)):
        (ltype, flags, feature_script_lang) = f.getLookupInfo(lookup_names[i])
        for (feat, script_langs) in feature_script_lang:
            if script_lang_match(script, langsys, script_langs):
                if feat not in features:
                    features[feat] = set()
                features[feat].add(i)
    return features

def make_feature_tables(features):

    if feature_RQD in features:
        record = {
            'featureTag' : feature_RQD,

            # FIX: Currently we offer no support for feature parameters.
            # Feature parameters for 'ss01', 'ss02', etc., can be ignored by us.
            # Feature parameters for 'size' might be useful.
            'featureParams' : 'NoFeatureParameters',

            'lookupListIndexes' : list(features[feature_RQD]),
            }
        required_feature = record
    else:
        required_feature = None

    feature_list = []
    for feat in features:
        if feat != feature_RQD:
            record = {
                'featureTag' : feat,
                'featureParams' : 'NoFeatureParameters',
                'lookupListIndexes' : list(features[feat]),
                }
            feature_list.append(record)

    langsys_table = {
        'lookupOrder' : 'NoReorderingTable', # FIX: Maybe one day we will need re-ordering tables.
        'reqFeature'  : required_feature,
        'features'    : feature_list,
        }

    return langsys_table

def get_feature_tables(f, table_name, script, langsys):
    try:
        if table_name == 'GSUB':
            lookup_names = f.gsub_lookups
        else:
            lookup_names = f.gpos_lookups
        features = get_features(f, lookup_names, unmake_tag(script), unmake_tag(langsys))
        feature_tables = make_feature_tables(features)
    except Exception, exc:
        print(traceback.print_exc())
        raise exc
    return feature_tables

def get_lookup_list(f, table_name):
    cdef libff.splinefont *sf = libff.sf(f)
    cdef libff.otlookup *lookups
    try:
        if table_name == 'GSUB':
            lookups = sf.gsub_lookups
        else:
            lookups = sf.gpos_lookups
        lookup_list = get_lookups(sf, lookups)
    except Exception, exc:
        print(traceback.print_exc())
        raise exc
    return lookup_list

def get_all_language_systems(f):
    language_systems = set()
    lookup_names = f.gsub_lookups + f.gpos_lookups
    for lname in lookup_names:
        (ltype, flags, feature_script_lang) = f.getLookupInfo(lname)
        for (feat, script_langs) in feature_script_lang:
            for (script, langs) in script_langs:
                for langsys in langs:
                    language_systems.add((script, langsys))
    return list(language_systems)

#--------------------------------------------------------------------------
